1   /*
2    * Copyright (c) 2000, 2005, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package com.sun.imageio.plugins.png;
27  
28  import java.awt.Rectangle;
29  import java.awt.image.ColorModel;
30  import java.awt.image.IndexColorModel;
31  import java.awt.image.Raster;
32  import java.awt.image.WritableRaster;
33  import java.awt.image.RenderedImage;
34  import java.awt.image.SampleModel;
35  import java.io.ByteArrayOutputStream;
36  import java.io.DataOutput;
37  import java.io.IOException;
38  import java.io.OutputStream;
39  import java.util.Iterator;
40  import java.util.Locale;
41  import java.util.zip.Deflater;
42  import java.util.zip.DeflaterOutputStream;
43  import javax.imageio.IIOException;
44  import javax.imageio.IIOImage;
45  import javax.imageio.ImageTypeSpecifier;
46  import javax.imageio.ImageWriteParam;
47  import javax.imageio.ImageWriter;
48  import javax.imageio.metadata.IIOMetadata;
49  import javax.imageio.metadata.IIOMetadata;
50  import javax.imageio.spi.ImageWriterSpi;
51  import javax.imageio.stream.ImageOutputStream;
52  import javax.imageio.stream.ImageOutputStreamImpl;
53  
54  class CRC {
55  
56      private static int[] crcTable = new int[256];
57      private int crc = 0xffffffff;
58  
59      static {
60          // Initialize CRC table
61          for (int n = 0; n < 256; n++) {
62              int c = n;
63              for (int k = 0; k < 8; k++) {
64                  if ((c & 1) == 1) {
65                      c = 0xedb88320 ^ (c >>> 1);
66                  } else {
67                      c >>>= 1;
68                  }
69  
70                  crcTable[n] = c;
71              }
72          }
73      }
74  
75      public CRC() {}
76  
77      public void reset() {
78          crc = 0xffffffff;
79      }
80  
81      public void update(byte[] data, int off, int len) {
82          for (int n = 0; n < len; n++) {
83              crc = crcTable[(crc ^ data[off + n]) & 0xff] ^ (crc >>> 8);
84          }
85      }
86  
87      public void update(int data) {
88          crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8);
89      }
90  
91      public int getValue() {
92          return crc ^ 0xffffffff;
93      }
94  }
95  
96  
97  final class ChunkStream extends ImageOutputStreamImpl {
98  
99      private ImageOutputStream stream;
100     private long startPos;
101     private CRC crc = new CRC();
102 
103     public ChunkStream(int type, ImageOutputStream stream) throws IOException {
104         this.stream = stream;
105         this.startPos = stream.getStreamPosition();
106 
107         stream.writeInt(-1); // length, will backpatch
108         writeInt(type);
109     }
110 
111     public int read() throws IOException {
112         throw new RuntimeException("Method not available");
113     }
114 
115     public int read(byte[] b, int off, int len) throws IOException {
116         throw new RuntimeException("Method not available");
117     }
118 
119     public void write(byte[] b, int off, int len) throws IOException {
120         crc.update(b, off, len);
121         stream.write(b, off, len);
122     }
123 
124     public void write(int b) throws IOException {
125         crc.update(b);
126         stream.write(b);
127     }
128 
129     public void finish() throws IOException {
130         // Write CRC
131         stream.writeInt(crc.getValue());
132 
133         // Write length
134         long pos = stream.getStreamPosition();
135         stream.seek(startPos);
136         stream.writeInt((int)(pos - startPos) - 12);
137 
138         // Return to end of chunk and flush to minimize buffering
139         stream.seek(pos);
140         stream.flushBefore(pos);
141     }
142 
143     protected void finalize() throws Throwable {
144         // Empty finalizer (for improved performance; no need to call
145         // super.finalize() in this case)
146     }
147 }
148 
149 // Compress output and write as a series of 'IDAT' chunks of
150 // fixed length.
151 final class IDATOutputStream extends ImageOutputStreamImpl {
152 
153     private static byte[] chunkType = {
154         (byte)'I', (byte)'D', (byte)'A', (byte)'T'
155     };
156 
157     private ImageOutputStream stream;
158     private int chunkLength;
159     private long startPos;
160     private CRC crc = new CRC();
161 
162     Deflater def = new Deflater(Deflater.BEST_COMPRESSION);
163     byte[] buf = new byte[512];
164 
165     private int bytesRemaining;
166 
167     public IDATOutputStream(ImageOutputStream stream, int chunkLength)
168         throws IOException {
169         this.stream = stream;
170         this.chunkLength = chunkLength;
171         startChunk();
172     }
173 
174     private void startChunk() throws IOException {
175         crc.reset();
176         this.startPos = stream.getStreamPosition();
177         stream.writeInt(-1); // length, will backpatch
178 
179         crc.update(chunkType, 0, 4);
180         stream.write(chunkType, 0, 4);
181 
182         this.bytesRemaining = chunkLength;
183     }
184 
185     private void finishChunk() throws IOException {
186         // Write CRC
187         stream.writeInt(crc.getValue());
188 
189         // Write length
190         long pos = stream.getStreamPosition();
191         stream.seek(startPos);
192         stream.writeInt((int)(pos - startPos) - 12);
193 
194         // Return to end of chunk and flush to minimize buffering
195         stream.seek(pos);
196         stream.flushBefore(pos);
197     }
198 
199     public int read() throws IOException {
200         throw new RuntimeException("Method not available");
201     }
202 
203     public int read(byte[] b, int off, int len) throws IOException {
204         throw new RuntimeException("Method not available");
205     }
206 
207     public void write(byte[] b, int off, int len) throws IOException {
208         if (len == 0) {
209             return;
210         }
211 
212         if (!def.finished()) {
213             def.setInput(b, off, len);
214             while (!def.needsInput()) {
215                 deflate();
216             }
217         }
218     }
219 
220     public void deflate() throws IOException {
221         int len = def.deflate(buf, 0, buf.length);
222         int off = 0;
223 
224         while (len > 0) {
225             if (bytesRemaining == 0) {
226                 finishChunk();
227                 startChunk();
228             }
229 
230             int nbytes = Math.min(len, bytesRemaining);
231             crc.update(buf, off, nbytes);
232             stream.write(buf, off, nbytes);
233 
234             off += nbytes;
235             len -= nbytes;
236             bytesRemaining -= nbytes;
237         }
238     }
239 
240     public void write(int b) throws IOException {
241         byte[] wbuf = new byte[1];
242         wbuf[0] = (byte)b;
243         write(wbuf, 0, 1);
244     }
245 
246     public void finish() throws IOException {
247         try {
248             if (!def.finished()) {
249                 def.finish();
250                 while (!def.finished()) {
251                     deflate();
252                 }
253             }
254             finishChunk();
255         } finally {
256             def.end();
257         }
258     }
259 
260     protected void finalize() throws Throwable {
261         // Empty finalizer (for improved performance; no need to call
262         // super.finalize() in this case)
263     }
264 }
265 
266 
267 class PNGImageWriteParam extends ImageWriteParam {
268 
269     public PNGImageWriteParam(Locale locale) {
270         super();
271         this.canWriteProgressive = true;
272         this.locale = locale;
273     }
274 }
275 
276 /**
277  */
278 public class PNGImageWriter extends ImageWriter {
279 
280     ImageOutputStream stream = null;
281 
282     PNGMetadata metadata = null;
283 
284     // Factors from the ImageWriteParam
285     int sourceXOffset = 0;
286     int sourceYOffset = 0;
287     int sourceWidth = 0;
288     int sourceHeight = 0;
289     int[] sourceBands = null;
290     int periodX = 1;
291     int periodY = 1;
292 
293     int numBands;
294     int bpp;
295 
296     RowFilter rowFilter = new RowFilter();
297     byte[] prevRow = null;
298     byte[] currRow = null;
299     byte[][] filteredRows = null;
300 
301     // Per-band scaling tables
302     //
303     // After the first call to initializeScaleTables, either scale and scale0
304     // will be valid, or scaleh and scalel will be valid, but not both.
305     //
306     // The tables will be designed for use with a set of input but depths
307     // given by sampleSize, and an output bit depth given by scalingBitDepth.
308     //
309     int[] sampleSize = null; // Sample size per band, in bits
310     int scalingBitDepth = -1; // Output bit depth of the scaling tables
311 
312     // Tables for 1, 2, 4, or 8 bit output
313     byte[][] scale = null; // 8 bit table
314     byte[] scale0 = null; // equivalent to scale[0]
315 
316     // Tables for 16 bit output
317     byte[][] scaleh = null; // High bytes of output
318     byte[][] scalel = null; // Low bytes of output
319 
320     int totalPixels; // Total number of pixels to be written by write_IDAT
321     int pixelsDone; // Running count of pixels written by write_IDAT
322 
323     public PNGImageWriter(ImageWriterSpi originatingProvider) {
324         super(originatingProvider);
325     }
326 
327     public void setOutput(Object output) {
328         super.setOutput(output);
329         if (output != null) {
330             if (!(output instanceof ImageOutputStream)) {
331                 throw new IllegalArgumentException("output not an ImageOutputStream!");
332             }
333             this.stream = (ImageOutputStream)output;
334         } else {
335             this.stream = null;
336         }
337     }
338 
339     private static int[] allowedProgressivePasses = { 1, 7 };
340 
341     public ImageWriteParam getDefaultWriteParam() {
342         return new PNGImageWriteParam(getLocale());
343     }
344 
345     public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
346         return null;
347     }
348 
349     public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
350                                                ImageWriteParam param) {
351         PNGMetadata m = new PNGMetadata();
352         m.initialize(imageType, imageType.getSampleModel().getNumBands());
353         return m;
354     }
355 
356     public IIOMetadata convertStreamMetadata(IIOMetadata inData,
357                                              ImageWriteParam param) {
358         return null;
359     }
360 
361     public IIOMetadata convertImageMetadata(IIOMetadata inData,
362                                             ImageTypeSpecifier imageType,
363                                             ImageWriteParam param) {
364         // TODO - deal with imageType
365         if (inData instanceof PNGMetadata) {
366             return (PNGMetadata)((PNGMetadata)inData).clone();
367         } else {
368             return new PNGMetadata(inData);
369         }
370     }
371 
372     private void write_magic() throws IOException {
373         // Write signature
374         byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 };
375         stream.write(magic);
376     }
377 
378     private void write_IHDR() throws IOException {
379         // Write IHDR chunk
380         ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream);
381         cs.writeInt(metadata.IHDR_width);
382         cs.writeInt(metadata.IHDR_height);
383         cs.writeByte(metadata.IHDR_bitDepth);
384         cs.writeByte(metadata.IHDR_colorType);
385         if (metadata.IHDR_compressionMethod != 0) {
386             throw new IIOException(
387 "Only compression method 0 is defined in PNG 1.1");
388         }
389         cs.writeByte(metadata.IHDR_compressionMethod);
390         if (metadata.IHDR_filterMethod != 0) {
391             throw new IIOException(
392 "Only filter method 0 is defined in PNG 1.1");
393         }
394         cs.writeByte(metadata.IHDR_filterMethod);
395         if (metadata.IHDR_interlaceMethod < 0 ||
396             metadata.IHDR_interlaceMethod > 1) {
397             throw new IIOException(
398 "Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1");
399         }
400         cs.writeByte(metadata.IHDR_interlaceMethod);
401         cs.finish();
402     }
403 
404     private void write_cHRM() throws IOException {
405         if (metadata.cHRM_present) {
406             ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream);
407             cs.writeInt(metadata.cHRM_whitePointX);
408             cs.writeInt(metadata.cHRM_whitePointY);
409             cs.writeInt(metadata.cHRM_redX);
410             cs.writeInt(metadata.cHRM_redY);
411             cs.writeInt(metadata.cHRM_greenX);
412             cs.writeInt(metadata.cHRM_greenY);
413             cs.writeInt(metadata.cHRM_blueX);
414             cs.writeInt(metadata.cHRM_blueY);
415             cs.finish();
416         }
417     }
418 
419     private void write_gAMA() throws IOException {
420         if (metadata.gAMA_present) {
421             ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream);
422             cs.writeInt(metadata.gAMA_gamma);
423             cs.finish();
424         }
425     }
426 
427     private void write_iCCP() throws IOException {
428         if (metadata.iCCP_present) {
429             ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream);
430             cs.writeBytes(metadata.iCCP_profileName);
431             cs.writeByte(0); // null terminator
432 
433             cs.writeByte(metadata.iCCP_compressionMethod);
434             cs.write(metadata.iCCP_compressedProfile);
435             cs.finish();
436         }
437     }
438 
439     private void write_sBIT() throws IOException {
440         if (metadata.sBIT_present) {
441             ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream);
442             int colorType = metadata.IHDR_colorType;
443             if (metadata.sBIT_colorType != colorType) {
444                 processWarningOccurred(0,
445 "sBIT metadata has wrong color type.\n" +
446 "The chunk will not be written.");
447                 return;
448             }
449 
450             if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
451                 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
452                 cs.writeByte(metadata.sBIT_grayBits);
453             } else if (colorType == PNGImageReader.PNG_COLOR_RGB ||
454                        colorType == PNGImageReader.PNG_COLOR_PALETTE ||
455                        colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
456                 cs.writeByte(metadata.sBIT_redBits);
457                 cs.writeByte(metadata.sBIT_greenBits);
458                 cs.writeByte(metadata.sBIT_blueBits);
459             }
460 
461             if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
462                 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
463                 cs.writeByte(metadata.sBIT_alphaBits);
464             }
465             cs.finish();
466         }
467     }
468 
469     private void write_sRGB() throws IOException {
470         if (metadata.sRGB_present) {
471             ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream);
472             cs.writeByte(metadata.sRGB_renderingIntent);
473             cs.finish();
474         }
475     }
476 
477     private void write_PLTE() throws IOException {
478         if (metadata.PLTE_present) {
479             if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY ||
480               metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
481                 // PLTE cannot occur in a gray image
482 
483                 processWarningOccurred(0,
484 "A PLTE chunk may not appear in a gray or gray alpha image.\n" +
485 "The chunk will not be written");
486                 return;
487             }
488 
489             ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream);
490 
491             int numEntries = metadata.PLTE_red.length;
492             byte[] palette = new byte[numEntries*3];
493             int index = 0;
494             for (int i = 0; i < numEntries; i++) {
495                 palette[index++] = metadata.PLTE_red[i];
496                 palette[index++] = metadata.PLTE_green[i];
497                 palette[index++] = metadata.PLTE_blue[i];
498             }
499 
500             cs.write(palette);
501             cs.finish();
502         }
503     }
504 
505     private void write_hIST() throws IOException, IIOException {
506         if (metadata.hIST_present) {
507             ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream);
508 
509             if (!metadata.PLTE_present) {
510                 throw new IIOException("hIST chunk without PLTE chunk!");
511             }
512 
513             cs.writeChars(metadata.hIST_histogram,
514                           0, metadata.hIST_histogram.length);
515             cs.finish();
516         }
517     }
518 
519     private void write_tRNS() throws IOException, IIOException {
520         if (metadata.tRNS_present) {
521             ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream);
522             int colorType = metadata.IHDR_colorType;
523             int chunkType = metadata.tRNS_colorType;
524 
525             // Special case: image is RGB and chunk is Gray
526             // Promote chunk contents to RGB
527             int chunkRed = metadata.tRNS_red;
528             int chunkGreen = metadata.tRNS_green;
529             int chunkBlue = metadata.tRNS_blue;
530             if (colorType == PNGImageReader.PNG_COLOR_RGB &&
531                 chunkType == PNGImageReader.PNG_COLOR_GRAY) {
532                 chunkType = colorType;
533                 chunkRed = chunkGreen = chunkBlue =
534                     metadata.tRNS_gray;
535             }
536 
537             if (chunkType != colorType) {
538                 processWarningOccurred(0,
539 "tRNS metadata has incompatible color type.\n" +
540 "The chunk will not be written.");
541                 return;
542             }
543 
544             if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
545                 if (!metadata.PLTE_present) {
546                     throw new IIOException("tRNS chunk without PLTE chunk!");
547                 }
548                 cs.write(metadata.tRNS_alpha);
549             } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) {
550                 cs.writeShort(metadata.tRNS_gray);
551             } else if (colorType == PNGImageReader.PNG_COLOR_RGB) {
552                 cs.writeShort(chunkRed);
553                 cs.writeShort(chunkGreen);
554                 cs.writeShort(chunkBlue);
555             } else {
556                 throw new IIOException("tRNS chunk for color type 4 or 6!");
557             }
558             cs.finish();
559         }
560     }
561 
562     private void write_bKGD() throws IOException {
563         if (metadata.bKGD_present) {
564             ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream);
565             int colorType = metadata.IHDR_colorType & 0x3;
566             int chunkType = metadata.bKGD_colorType;
567 
568             // Special case: image is RGB(A) and chunk is Gray
569             // Promote chunk contents to RGB
570             int chunkRed = metadata.bKGD_red;
571             int chunkGreen = metadata.bKGD_red;
572             int chunkBlue = metadata.bKGD_red;
573             if (colorType == PNGImageReader.PNG_COLOR_RGB &&
574                 chunkType == PNGImageReader.PNG_COLOR_GRAY) {
575                 // Make a gray bKGD chunk look like RGB
576                 chunkType = colorType;
577                 chunkRed = chunkGreen = chunkBlue =
578                     metadata.bKGD_gray;
579             }
580 
581             // Ignore status of alpha in colorType
582             if (chunkType != colorType) {
583                 processWarningOccurred(0,
584 "bKGD metadata has incompatible color type.\n" +
585 "The chunk will not be written.");
586                 return;
587             }
588 
589             if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
590                 cs.writeByte(metadata.bKGD_index);
591             } else if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
592                        colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
593                 cs.writeShort(metadata.bKGD_gray);
594             } else { // colorType == PNGImageReader.PNG_COLOR_RGB ||
595                      // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
596                 cs.writeShort(chunkRed);
597                 cs.writeShort(chunkGreen);
598                 cs.writeShort(chunkBlue);
599             }
600             cs.finish();
601         }
602     }
603 
604     private void write_pHYs() throws IOException {
605         if (metadata.pHYs_present) {
606             ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream);
607             cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis);
608             cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis);
609             cs.writeByte(metadata.pHYs_unitSpecifier);
610             cs.finish();
611         }
612     }
613 
614     private void write_sPLT() throws IOException {
615         if (metadata.sPLT_present) {
616             ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream);
617 
618             cs.writeBytes(metadata.sPLT_paletteName);
619             cs.writeByte(0); // null terminator
620 
621             cs.writeByte(metadata.sPLT_sampleDepth);
622             int numEntries = metadata.sPLT_red.length;
623 
624             if (metadata.sPLT_sampleDepth == 8) {
625                 for (int i = 0; i < numEntries; i++) {
626                     cs.writeByte(metadata.sPLT_red[i]);
627                     cs.writeByte(metadata.sPLT_green[i]);
628                     cs.writeByte(metadata.sPLT_blue[i]);
629                     cs.writeByte(metadata.sPLT_alpha[i]);
630                     cs.writeShort(metadata.sPLT_frequency[i]);
631                 }
632             } else { // sampleDepth == 16
633                 for (int i = 0; i < numEntries; i++) {
634                     cs.writeShort(metadata.sPLT_red[i]);
635                     cs.writeShort(metadata.sPLT_green[i]);
636                     cs.writeShort(metadata.sPLT_blue[i]);
637                     cs.writeShort(metadata.sPLT_alpha[i]);
638                     cs.writeShort(metadata.sPLT_frequency[i]);
639                 }
640             }
641             cs.finish();
642         }
643     }
644 
645     private void write_tIME() throws IOException {
646         if (metadata.tIME_present) {
647             ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream);
648             cs.writeShort(metadata.tIME_year);
649             cs.writeByte(metadata.tIME_month);
650             cs.writeByte(metadata.tIME_day);
651             cs.writeByte(metadata.tIME_hour);
652             cs.writeByte(metadata.tIME_minute);
653             cs.writeByte(metadata.tIME_second);
654             cs.finish();
655         }
656     }
657 
658     private void write_tEXt() throws IOException {
659         Iterator keywordIter = metadata.tEXt_keyword.iterator();
660         Iterator textIter = metadata.tEXt_text.iterator();
661 
662         while (keywordIter.hasNext()) {
663             ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
664             String keyword = (String)keywordIter.next();
665             cs.writeBytes(keyword);
666             cs.writeByte(0);
667 
668             String text = (String)textIter.next();
669             cs.writeBytes(text);
670             cs.finish();
671         }
672     }
673 
674     private byte[] deflate(byte[] b) throws IOException {
675         ByteArrayOutputStream baos = new ByteArrayOutputStream();
676         DeflaterOutputStream dos = new DeflaterOutputStream(baos);
677         dos.write(b);
678         dos.close();
679         return baos.toByteArray();
680     }
681 
682     private void write_iTXt() throws IOException {
683         Iterator<String> keywordIter = metadata.iTXt_keyword.iterator();
684         Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator();
685         Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator();
686         Iterator<String> languageIter = metadata.iTXt_languageTag.iterator();
687         Iterator<String> translatedKeywordIter =
688             metadata.iTXt_translatedKeyword.iterator();
689         Iterator<String> textIter = metadata.iTXt_text.iterator();
690 
691         while (keywordIter.hasNext()) {
692             ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream);
693 
694             cs.writeBytes(keywordIter.next());
695             cs.writeByte(0);
696 
697             Boolean compressed = flagIter.next();
698             cs.writeByte(compressed ? 1 : 0);
699 
700             cs.writeByte(methodIter.next().intValue());
701 
702             cs.writeBytes(languageIter.next());
703             cs.writeByte(0);
704 
705 
706             cs.write(translatedKeywordIter.next().getBytes("UTF8"));
707             cs.writeByte(0);
708 
709             String text = textIter.next();
710             if (compressed) {
711                 cs.write(deflate(text.getBytes("UTF8")));
712             } else {
713                 cs.write(text.getBytes("UTF8"));
714             }
715             cs.finish();
716         }
717     }
718 
719     private void write_zTXt() throws IOException {
720         Iterator keywordIter = metadata.zTXt_keyword.iterator();
721         Iterator methodIter = metadata.zTXt_compressionMethod.iterator();
722         Iterator textIter = metadata.zTXt_text.iterator();
723 
724         while (keywordIter.hasNext()) {
725             ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
726             String keyword = (String)keywordIter.next();
727             cs.writeBytes(keyword);
728             cs.writeByte(0);
729 
730             int compressionMethod = ((Integer)methodIter.next()).intValue();
731             cs.writeByte(compressionMethod);
732 
733             String text = (String)textIter.next();
734             cs.write(deflate(text.getBytes("ISO-8859-1")));
735             cs.finish();
736         }
737     }
738 
739     private void writeUnknownChunks() throws IOException {
740         Iterator typeIter = metadata.unknownChunkType.iterator();
741         Iterator dataIter = metadata.unknownChunkData.iterator();
742 
743         while (typeIter.hasNext() && dataIter.hasNext()) {
744             String type = (String)typeIter.next();
745             ChunkStream cs = new ChunkStream(chunkType(type), stream);
746             byte[] data = (byte[])dataIter.next();
747             cs.write(data);
748             cs.finish();
749         }
750     }
751 
752     private static int chunkType(String typeString) {
753         char c0 = typeString.charAt(0);
754         char c1 = typeString.charAt(1);
755         char c2 = typeString.charAt(2);
756         char c3 = typeString.charAt(3);
757 
758         int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3;
759         return type;
760     }
761 
762     private void encodePass(ImageOutputStream os,
763                             RenderedImage image,
764                             int xOffset, int yOffset,
765                             int xSkip, int ySkip) throws IOException {
766         int minX = sourceXOffset;
767         int minY = sourceYOffset;
768         int width = sourceWidth;
769         int height = sourceHeight;
770 
771         // Adjust offsets and skips based on source subsampling factors
772         xOffset *= periodX;
773         xSkip *= periodX;
774         yOffset *= periodY;
775         ySkip *= periodY;
776 
777         // Early exit if no data for this pass
778         int hpixels = (width - xOffset + xSkip - 1)/xSkip;
779         int vpixels = (height - yOffset + ySkip - 1)/ySkip;
780         if (hpixels == 0 || vpixels == 0) {
781             return;
782         }
783 
784         // Convert X offset and skip from pixels to samples
785         xOffset *= numBands;
786         xSkip *= numBands;
787 
788         // Create row buffers
789         int samplesPerByte = 8/metadata.IHDR_bitDepth;
790         int numSamples = width*numBands;
791         int[] samples = new int[numSamples];
792 
793         int bytesPerRow = hpixels*numBands;
794         if (metadata.IHDR_bitDepth < 8) {
795             bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte;
796         } else if (metadata.IHDR_bitDepth == 16) {
797             bytesPerRow *= 2;
798         }
799 
800         IndexColorModel icm_gray_alpha = null;
801         if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA &&
802             image.getColorModel() instanceof IndexColorModel)
803         {
804             // reserve space for alpha samples
805             bytesPerRow *= 2;
806 
807             // will be used to calculate alpha value for the pixel
808             icm_gray_alpha = (IndexColorModel)image.getColorModel();
809         }
810 
811         currRow = new byte[bytesPerRow + bpp];
812         prevRow = new byte[bytesPerRow + bpp];
813         filteredRows = new byte[5][bytesPerRow + bpp];
814 
815         int bitDepth = metadata.IHDR_bitDepth;
816         for (int row = minY + yOffset; row < minY + height; row += ySkip) {
817             Rectangle rect = new Rectangle(minX, row, width, 1);
818             Raster ras = image.getData(rect);
819             if (sourceBands != null) {
820                 ras = ras.createChild(minX, row, width, 1, minX, row,
821                                       sourceBands);
822             }
823 
824             ras.getPixels(minX, row, width, 1, samples);
825 
826             if (image.getColorModel().isAlphaPremultiplied()) {
827                 WritableRaster wr = ras.createCompatibleWritableRaster();
828                 wr.setPixels(wr.getMinX(), wr.getMinY(),
829                              wr.getWidth(), wr.getHeight(),
830                              samples);
831 
832                 image.getColorModel().coerceData(wr, false);
833                 wr.getPixels(wr.getMinX(), wr.getMinY(),
834                              wr.getWidth(), wr.getHeight(),
835                              samples);
836             }
837 
838             // Reorder palette data if necessary
839             int[] paletteOrder = metadata.PLTE_order;
840             if (paletteOrder != null) {
841                 for (int i = 0; i < numSamples; i++) {
842                     samples[i] = paletteOrder[samples[i]];
843                 }
844             }
845 
846             int count = bpp; // leave first 'bpp' bytes zero
847             int pos = 0;
848             int tmp = 0;
849 
850             switch (bitDepth) {
851             case 1: case 2: case 4:
852                 // Image can only have a single band
853 
854                 int mask = samplesPerByte - 1;
855                 for (int s = xOffset; s < numSamples; s += xSkip) {
856                     byte val = scale0[samples[s]];
857                     tmp = (tmp << bitDepth) | val;
858 
859                     if ((pos++ & mask) == mask) {
860                         currRow[count++] = (byte)tmp;
861                         tmp = 0;
862                         pos = 0;
863                     }
864                 }
865 
866                 // Left shift the last byte
867                 if ((pos & mask) != 0) {
868                     tmp <<= ((8/bitDepth) - pos)*bitDepth;
869                     currRow[count++] = (byte)tmp;
870                 }
871                 break;
872 
873             case 8:
874                 if (numBands == 1) {
875                     for (int s = xOffset; s < numSamples; s += xSkip) {
876                         currRow[count++] = scale0[samples[s]];
877                         if (icm_gray_alpha != null) {
878                             currRow[count++] =
879                                 scale0[icm_gray_alpha.getAlpha(0xff & samples[s])];
880                         }
881                     }
882                 } else {
883                     for (int s = xOffset; s < numSamples; s += xSkip) {
884                         for (int b = 0; b < numBands; b++) {
885                             currRow[count++] = scale[b][samples[s + b]];
886                         }
887                     }
888                 }
889                 break;
890 
891             case 16:
892                 for (int s = xOffset; s < numSamples; s += xSkip) {
893                     for (int b = 0; b < numBands; b++) {
894                         currRow[count++] = scaleh[b][samples[s + b]];
895                         currRow[count++] = scalel[b][samples[s + b]];
896                     }
897                 }
898                 break;
899             }
900 
901             // Perform filtering
902             int filterType = rowFilter.filterRow(metadata.IHDR_colorType,
903                                                  currRow, prevRow,
904                                                  filteredRows,
905                                                  bytesPerRow, bpp);
906 
907             os.write(filterType);
908             os.write(filteredRows[filterType], bpp, bytesPerRow);
909 
910             // Swap current and previous rows
911             byte[] swap = currRow;
912             currRow = prevRow;
913             prevRow = swap;
914 
915             pixelsDone += hpixels;
916             processImageProgress(100.0F*pixelsDone/totalPixels);
917 
918             // If write has been aborted, just return;
919             // processWriteAborted will be called later
920             if (abortRequested()) {
921                 return;
922             }
923         }
924     }
925 
926     // Use sourceXOffset, etc.
927     private void write_IDAT(RenderedImage image) throws IOException {
928         IDATOutputStream ios = new IDATOutputStream(stream, 32768);
929         try {
930             if (metadata.IHDR_interlaceMethod == 1) {
931                 for (int i = 0; i < 7; i++) {
932                     encodePass(ios, image,
933                                PNGImageReader.adam7XOffset[i],
934                                PNGImageReader.adam7YOffset[i],
935                                PNGImageReader.adam7XSubsampling[i],
936                                PNGImageReader.adam7YSubsampling[i]);
937                     if (abortRequested()) {
938                         break;
939                     }
940                 }
941             } else {
942                 encodePass(ios, image, 0, 0, 1, 1);
943             }
944         } finally {
945             ios.finish();
946         }
947     }
948 
949     private void writeIEND() throws IOException {
950         ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream);
951         cs.finish();
952     }
953 
954     // Check two int arrays for value equality, always returns false
955     // if either array is null
956     private boolean equals(int[] s0, int[] s1) {
957         if (s0 == null || s1 == null) {
958             return false;
959         }
960         if (s0.length != s1.length) {
961             return false;
962         }
963         for (int i = 0; i < s0.length; i++) {
964             if (s0[i] != s1[i]) {
965                 return false;
966             }
967         }
968         return true;
969     }
970 
971     // Initialize the scale/scale0 or scaleh/scalel arrays to
972     // hold the results of scaling an input value to the desired
973     // output bit depth
974     private void initializeScaleTables(int[] sampleSize) {
975         int bitDepth = metadata.IHDR_bitDepth;
976 
977         // If the existing tables are still valid, just return
978         if (bitDepth == scalingBitDepth &&
979             equals(sampleSize, this.sampleSize)) {
980             return;
981         }
982 
983         // Compute new tables
984         this.sampleSize = sampleSize;
985         this.scalingBitDepth = bitDepth;
986         int maxOutSample = (1 << bitDepth) - 1;
987         if (bitDepth <= 8) {
988             scale = new byte[numBands][];
989             for (int b = 0; b < numBands; b++) {
990                 int maxInSample = (1 << sampleSize[b]) - 1;
991                 int halfMaxInSample = maxInSample/2;
992                 scale[b] = new byte[maxInSample + 1];
993                 for (int s = 0; s <= maxInSample; s++) {
994                     scale[b][s] =
995                         (byte)((s*maxOutSample + halfMaxInSample)/maxInSample);
996                 }
997             }
998             scale0 = scale[0];
999             scaleh = scalel = null;
1000         } else { // bitDepth == 16
1001             // Divide scaling table into high and low bytes
1002             scaleh = new byte[numBands][];
1003             scalel = new byte[numBands][];
1004 
1005             for (int b = 0; b < numBands; b++) {
1006                 int maxInSample = (1 << sampleSize[b]) - 1;
1007                 int halfMaxInSample = maxInSample/2;
1008                 scaleh[b] = new byte[maxInSample + 1];
1009                 scalel[b] = new byte[maxInSample + 1];
1010                 for (int s = 0; s <= maxInSample; s++) {
1011                     int val = (s*maxOutSample + halfMaxInSample)/maxInSample;
1012                     scaleh[b][s] = (byte)(val >> 8);
1013                     scalel[b][s] = (byte)(val & 0xff);
1014                 }
1015             }
1016             scale = null;
1017             scale0 = null;
1018         }
1019     }
1020 
1021     public void write(IIOMetadata streamMetadata,
1022                       IIOImage image,
1023                       ImageWriteParam param) throws IIOException {
1024         if (stream == null) {
1025             throw new IllegalStateException("output == null!");
1026         }
1027         if (image == null) {
1028             throw new IllegalArgumentException("image == null!");
1029         }
1030         if (image.hasRaster()) {
1031             throw new UnsupportedOperationException("image has a Raster!");
1032         }
1033 
1034         RenderedImage im = image.getRenderedImage();
1035         SampleModel sampleModel = im.getSampleModel();
1036         this.numBands = sampleModel.getNumBands();
1037 
1038         // Set source region and subsampling to default values
1039         this.sourceXOffset = im.getMinX();
1040         this.sourceYOffset = im.getMinY();
1041         this.sourceWidth = im.getWidth();
1042         this.sourceHeight = im.getHeight();
1043         this.sourceBands = null;
1044         this.periodX = 1;
1045         this.periodY = 1;
1046 
1047         if (param != null) {
1048             // Get source region and subsampling factors
1049             Rectangle sourceRegion = param.getSourceRegion();
1050             if (sourceRegion != null) {
1051                 Rectangle imageBounds = new Rectangle(im.getMinX(),
1052                                                       im.getMinY(),
1053                                                       im.getWidth(),
1054                                                       im.getHeight());
1055                 // Clip to actual image bounds
1056                 sourceRegion = sourceRegion.intersection(imageBounds);
1057                 sourceXOffset = sourceRegion.x;
1058                 sourceYOffset = sourceRegion.y;
1059                 sourceWidth = sourceRegion.width;
1060                 sourceHeight = sourceRegion.height;
1061             }
1062 
1063             // Adjust for subsampling offsets
1064             int gridX = param.getSubsamplingXOffset();
1065             int gridY = param.getSubsamplingYOffset();
1066             sourceXOffset += gridX;
1067             sourceYOffset += gridY;
1068             sourceWidth -= gridX;
1069             sourceHeight -= gridY;
1070 
1071             // Get subsampling factors
1072             periodX = param.getSourceXSubsampling();
1073             periodY = param.getSourceYSubsampling();
1074 
1075             int[] sBands = param.getSourceBands();
1076             if (sBands != null) {
1077                 sourceBands = sBands;
1078                 numBands = sourceBands.length;
1079             }
1080         }
1081 
1082         // Compute output dimensions
1083         int destWidth = (sourceWidth + periodX - 1)/periodX;
1084         int destHeight = (sourceHeight + periodY - 1)/periodY;
1085         if (destWidth <= 0 || destHeight <= 0) {
1086             throw new IllegalArgumentException("Empty source region!");
1087         }
1088 
1089         // Compute total number of pixels for progress notification
1090         this.totalPixels = destWidth*destHeight;
1091         this.pixelsDone = 0;
1092 
1093         // Create metadata
1094         IIOMetadata imd = image.getMetadata();
1095         if (imd != null) {
1096             metadata = (PNGMetadata)convertImageMetadata(imd,
1097                                ImageTypeSpecifier.createFromRenderedImage(im),
1098                                                          null);
1099         } else {
1100             metadata = new PNGMetadata();
1101         }
1102 
1103         if (param != null) {
1104             // Use Adam7 interlacing if set in write param
1105             switch (param.getProgressiveMode()) {
1106             case ImageWriteParam.MODE_DEFAULT:
1107                 metadata.IHDR_interlaceMethod = 1;
1108                 break;
1109             case ImageWriteParam.MODE_DISABLED:
1110                 metadata.IHDR_interlaceMethod = 0;
1111                 break;
1112                 // MODE_COPY_FROM_METADATA should alreay be taken care of
1113                 // MODE_EXPLICIT is not allowed
1114             }
1115         }
1116 
1117         // Initialize bitDepth and colorType
1118         metadata.initialize(new ImageTypeSpecifier(im), numBands);
1119 
1120         // Overwrite IHDR width and height values with values from image
1121         metadata.IHDR_width = destWidth;
1122         metadata.IHDR_height = destHeight;
1123 
1124         this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1);
1125 
1126         // Initialize scaling tables for this image
1127         initializeScaleTables(sampleModel.getSampleSize());
1128 
1129         clearAbortRequest();
1130 
1131         processImageStarted(0);
1132 
1133         try {
1134             write_magic();
1135             write_IHDR();
1136 
1137             write_cHRM();
1138             write_gAMA();
1139             write_iCCP();
1140             write_sBIT();
1141             write_sRGB();
1142 
1143             write_PLTE();
1144 
1145             write_hIST();
1146             write_tRNS();
1147             write_bKGD();
1148 
1149             write_pHYs();
1150             write_sPLT();
1151             write_tIME();
1152             write_tEXt();
1153             write_iTXt();
1154             write_zTXt();
1155 
1156             writeUnknownChunks();
1157 
1158             write_IDAT(im);
1159 
1160             if (abortRequested()) {
1161                 processWriteAborted();
1162             } else {
1163                 // Finish up and inform the listeners we are done
1164                 writeIEND();
1165                 processImageComplete();
1166             }
1167         } catch (IOException e) {
1168             throw new IIOException("I/O error writing PNG file!", e);
1169         }
1170     }
1171 }